{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# Artist Picking\n",
    "\n",
    "This tutorial will go through how to configure a callback using `'pick_event'` to drill into a data set (in this case the Titanic passanger manifest).  For a more through tutorial please see [the full interactive tutorial](https://github.com/matplotlib/interactive_tutorial) and the [API documentation](\n",
    "https://matplotlib.org/3.1.0/users/event_handling.html).\n",
    "\n",
    "This tutorial shows how to build a interaction customized to this visualization of this data set, for sets of high-level general tools see [mpldatacursors](https://github.com/joferkington/mpldatacursor/) and [mplcursors](https://github.com/anntzer/mplcursors)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "%matplotlib notebook\n",
    "%run helpers/ensure_print.py"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "from matplotlib.backend_bases import MouseButton\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "data = pd.read_csv(\"http://bit.ly/tscv17\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "skip"
    }
   },
   "source": [
    "## Our plotting function\n",
    "\n",
    "The first step is to make our selves a helper function that will \"do the right thing\" for our data.  To start with, this is a wrapper around `scatter`, but eventually this will also define and install the interactive functions.\n",
    "\n",
    "We are using the `legend_elements` which was added in [Matplotlib 3.1](https://matplotlib.org/users/prev_whats_new/whats_new_3.1.0.html#legend-for-scatter) to get a nice legend for the data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "def make_plot(ax, data, *, x_data='Age', y_data='Fare', c_data='Survived'):\n",
    "    # note use of 'data kwarg' new in mpl 1.5\n",
    "    sc = ax.scatter(x_data, y_data, c=c_data, data=data, picker=5, alpha=.75) \n",
    "    ax.set(xlabel=x_data, ylabel=y_data)\n",
    "    # mpl 3.1 feature\n",
    "    ax.legend(*sc.legend_elements(), title=c_data)\n",
    "    \n",
    "fig, ax = plt.subplots()\n",
    "make_plot(ax, data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Print a row\n",
    "\n",
    "To get a bit more information about each point we can use the `'pick_event'` to print information about each point (aka row) to the screen.  By passing the `picker=5` to `ax.scatter` we enable picking on the resulting `PathCollection` artist.  The units on `picker` is pixels.  We can then register a callback to be called when ever the user clicks on (aka 'picks') the `Artist`.\n",
    "\n",
    "[`PickEvent`](https://matplotlib.org/3.1.1/api/backend_bases_api.html#matplotlib.backend_bases.PickEvent) gives us access to the underlying [`MouseEvent`](https://matplotlib.org/3.1.1/api/backend_bases_api.html#matplotlib.backend_bases.MouseEvent), the `Artist` instance that was picked, and depending on the artist some additional information.  In the case of `PathCollection` (from `scatter`) and `Line2D` (from `plot`) the `PickEvent` has and `ind` attribute which is a list of the positional indexes in the input data that was picked.\n",
    "\n",
    "The attributes on the `event` that we are going to primarily conecerned with are:\n",
    "\n",
    "```python\n",
    "def callback(event : PickEvent) -> None:\n",
    "    mouse_event = event.mousevent # the underlying mouse event\n",
    "    button = mouse_event.button   # the button clicked as an Enum\n",
    "    ind = event.ind               # list of index of data point picked\n",
    "\n",
    "```\n",
    "\n",
    "\n",
    "Additional information (which `Axes`, the (x, y) values, keyboard keys held down) can be extract from `mouse_event` (see previous tutorial)\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "def make_plot(ax, data, *, x_data='Age', y_data='Fare', c_data='Survived'):\n",
    "\n",
    "    sc = ax.scatter(x_data, y_data, c=c_data, data=data, picker=5, alpha=.75)\n",
    "    ax.set(xlabel=x_data, ylabel=y_data)\n",
    "    ax.legend(*sc.legend_elements(), title=c_data)\n",
    "    \n",
    "    # Define an inner function.  This will \"close over\" the input `data`\n",
    "    # and our newly created artist `sc`\n",
    "    def print_row(event):\n",
    "        print(f'in a pick event! {event.ind} with mouse button {event.mouseevent.button}')\n",
    "        # make sure we are picking the artist we care about and the left mouse button!\n",
    "        if event.artist is not sc or event.mouseevent.button != MouseButton.LEFT: \n",
    "            return \n",
    "\n",
    "        for indx in event.ind:\n",
    "            # use iloc to select a row by numeric index\n",
    "            row = data.iloc[indx]\n",
    "            # TODO: print something more interesting!\n",
    "            print(f\"hit row {indx}\")\n",
    "    \n",
    "    # connect our call back to the canvas\n",
    "    ax.figure.canvas.mpl_connect('pick_event', print_row) \n",
    "    return sc\n",
    "    \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots()\n",
    "make_plot(ax, data);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "skip"
    }
   },
   "source": [
    "## Add an annotation\n",
    "\n",
    "We can do better than printing though, via [`ax.anotate`](https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.annotate.html) we can add annotations directly to the plot.  The API an `annotate` is large, in this case we are:\n",
    "\n",
    "- offsetting the text by \\pm 50pt in each direction from the data point\n",
    "- putting a transparent gray box around the text\n",
    "- connect the text to the data point with a curved arrow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "def make_plot(ax, data, *, x_data='Age', y_data='Fare', c_data='Survived'):\n",
    "    sc = ax.scatter(x_data, y_data, c=c_data, data=data, picker=5) \n",
    "    ax.set(xlabel=x_data, ylabel=y_data)\n",
    "    ax.legend(*sc.legend_elements(), \n",
    "                loc=\"best\", title=c_data,  \n",
    "                ncol=1)\n",
    "    \n",
    "    def add_annotation(event):\n",
    "        # if this is not our artist, bail\n",
    "        if event.artist is not sc or event.mouseevent.button != MouseButton.LEFT: \n",
    "            return \n",
    "        for indx in event.ind:\n",
    "            # grab the row\n",
    "            row = data.iloc[indx] \n",
    "            # format everything into a multi-line string\n",
    "            txt = '\\n'.join(['hit row', f'{indx}']) \n",
    "            ann = ax.annotate(s=txt, \n",
    "                              # update this to point someplace more sensible!\n",
    "                              xy=(0, 0),  \n",
    "                              # styling\n",
    "                              # make the box light gray and transparent\n",
    "                              bbox={'color': '.1', 'alpha': .2},\n",
    "                              # offset text randomly by a few points\n",
    "                              xytext=(np.random.rand(2) - .5) * 100,\n",
    "                              textcoords='offset points', \n",
    "                              # connect text to point with a curved arrow\n",
    "                              arrowprops=dict(\n",
    "                                  arrowstyle=\"->\", \n",
    "                                  connectionstyle=\"angle3,angleA=0,angleB=-90\") \n",
    "                ) \n",
    "            # make it movable!\n",
    "            ann.draggable()\n",
    "            \n",
    "        # trigger a re-draw at the next possible time\n",
    "        ax.figure.canvas.draw_idle() \n",
    "    # connect our function\n",
    "    ax.figure.canvas.mpl_connect('pick_event', add_annotation) \n",
    " \n",
    "fig, ax = plt.subplots()\n",
    "make_plot(ax, data)  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "### Exercise\n",
    "\n",
    "Adjust the annotaiton to point at the data point (not (0, 0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Bonus: remove the annotations\n",
    "\n",
    "If we can _add_ annotations, we should also be able to remove them.  All Matplotlib aritsts have a `art.remove()` method that will remove them from their parent in the draw tree.  Thus, with a bit of book-keeping we can remove the annotation on right (aka middle) click"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "def make_plot(ax, data, *, x_data='Age', y_data='Fare', c_data='Survived'):\n",
    "\n",
    "    sc = ax.scatter(x_data, y_data, c=c_data, data=data, picker=5, alpha=.75) \n",
    "    ax.set_xlabel(x_data)\n",
    "    ax.set_ylabel(y_data)\n",
    "    ax.legend(*sc.legend_elements(), title=c_data)\n",
    "    # someplace to stash our Annotation artists\n",
    "    annotations = set()\n",
    "    def add_annotation(event):\n",
    "        if event.artist is not sc: \n",
    "            return \n",
    "        if event.mouseevent.button != MouseButton.LEFT: \n",
    "            return \n",
    "        \n",
    "        for indx in event.ind: \n",
    "            row = data.iloc[indx] \n",
    "            txt = '\\n'.join(f'{k}: {v}' for k, v in row.items()) \n",
    "\n",
    "            ann = ax.annotate(txt, \n",
    "                              (row[x_data], row[y_data]),  \n",
    "                              # make the annotation also pickable\n",
    "                              picker=1,\n",
    "                              # style\n",
    "                              bbox={'color': '.1', 'alpha': .2},  \n",
    "                              xytext=(np.random.rand(2) - .5) * 100, \n",
    "                              textcoords='offset points', \n",
    "                              arrowprops=dict(\n",
    "                                  arrowstyle=\"->\", \n",
    "                                  connectionstyle=\"angle3,angleA=0,angleB=-90\") \n",
    "                ) \n",
    "            ann.draggable()\n",
    "            # also add the artist to the stash\n",
    "            annotations.add(ann)\n",
    "        ax.figure.canvas.draw_idle()\n",
    "    \n",
    "    # call back for _removing_ the annotations\n",
    "    def remove_annotation(event): \n",
    "        # if the artist is not in annotations, bail!\n",
    "        if event.artist not in annotations: \n",
    "            return \n",
    "        \n",
    "        if event.mouseevent.button == MouseButton.RIGHT:\n",
    "            # grab the artist to be efficent\n",
    "            art = event.artist\n",
    "            # remove the annotation from the figure\n",
    "            art.remove()\n",
    "            # remove the annotation from our stash\n",
    "            annotations.remove(art)\n",
    "            # ask the figure to re-draw\n",
    "            ax.figure.canvas.draw_idle() \n",
    " \n",
    "    # register both of the callbacks\n",
    "    ax.figure.canvas.mpl_connect('pick_event', add_annotation)\n",
    "    ax.figure.canvas.mpl_connect('pick_event', remove_annotation)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots()\n",
    "make_plot(ax, data)  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "celltoolbar": "Slideshow",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  },
  "latex_envs": {
   "LaTeX_envs_menu_present": true,
   "autoclose": false,
   "autocomplete": true,
   "bibliofile": "biblio.bib",
   "cite_by": "apalike",
   "current_citInitial": 1,
   "eqLabelWithNumbers": true,
   "eqNumInitial": 1,
   "hotkeys": {
    "equation": "Ctrl-E",
    "itemize": "Ctrl-I"
   },
   "labels_anchors": false,
   "latex_user_defs": false,
   "report_style_numbering": false,
   "user_envs_cfg": false
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
